[UE]夺旗模式框架
夺旗模式框架
描述
夺旗模式:
场景里会按照一定的规则刷新出不同类型的旗帜(不同类型的旗帜有不同的得分、血量、表现等);
玩家会被分成若干个队伍,抢夺刷新出来的旗帜,将旗帜带到自己队伍目标点进行销毁,则可以获得分数;
玩家持有旗帜时,旗帜可能会掉落或被抢夺:被其它玩家撞击到则旗帜被抢夺到其它玩家身上;旗帜的血量归零则会掉落回场景;
有正式赛与加时赛两种类型的比赛,首先会开启正式赛;对于正式赛,率先达到目标分的队伍获胜;如果平局则开启加时赛,否则进行结算;对于加时赛,率先得分的队伍获胜,如果平局则以两队平局进行结算;
要点总结:
- 旗帜实体:旗帜本身有自己的类型、得分、血量;
- 旗帜刷新:场景内根据一定规则(倒计时刷新、旗子上缴)会刷出不同类型旗帜;
- 旗帜传递:玩家可以拾取场景里的旗帜、相互抢夺旗帜、掉落旗帜(持有旗帜血量归零)、将旗帜送给终点得分等;玩家可以组队,共享队伍得分;同队伍之间的玩家可以传递旗帜;
- 单局流程:所有玩家就绪分队后,传送到起始点准备开赛;如果一局比赛平分,一定条件下可以触发下一场比赛继续;
模式基础
单局流程
首先,一个完整的夺旗赛,由若干个子比赛构成,子比赛逐渐推进;
flowchart LR M[MainProcess] R0[RaceProcess_0] R1[RaceProcess_1] R2[RaceProcess_2] M-->R0 M-->R1 M-->R2 R0-.->R1 R1-.->R2
比赛胜负规则:
单场比赛结束时,判定是否有一方队伍得分更高:
如果有,则全局结束;
如果没有,尝试进入下一场比赛(如果有配置的话),若没有下一场则全局结束;
- 正式赛:比赛结束条件:有一队达到目标分 / 倒计时结束
- 加时赛:比赛结束条件:有一队得到分数 / 倒计时结束
- 其它类型比赛……
局内玩法
从业务出发,先明确有哪些相关逻辑:
最基本的交互显然是:
flowchart LR F0[场景旗帜_0] F1[场景旗帜_1] G0([目标点_0]) G1([目标点_1]) P0[[Player_0]] P1[[Player_1]] F0--PickUp--->P0--Drop-->F0 P0--Reach--->G0 P0<--Strike--->P1
PickUp
:Player
触碰到场景旗帜
,就可以拾取该旗帜到自己身上;
Drop
:Player
持有的旗帜存在血量,其血量归零(可能是被其它Player
攻击导致扣血),就会触发旗帜的掉落,在对应位置生成场景旗帜;
Reach
:Player
将持有的旗帜护送到目标点
,获得分数,同时该旗帜会被销毁;
Strike
:Player
撞击另一个Player
,可以抢夺对方的旗帜;
业务拆分
首先,先给出基本的一些元素定义;
游戏玩法的本质是由刷新点刷新出旗帜,旗帜在各个部分之间交互传递,旗帜本身定义为
FlagItem
。旗帜在场景(不在
Player
身上)里的表现为,存在对应的FlagSceneItem
。场景里存在若干个终点,定义该终点为
GoalPointSceneItem
这里说的 交互 的本质实际上是一个 Flag
的 Instance
在 各个部分 之间的 传递 。想一下所谓的 各个部分,到底是什么?
本质可以抽象为一个个持有 Flag
的容器,将其定义为 FlagContainer
。
这里显然有三种 Container
:
Container_WorldFlag
:定义WorldFlag
为 没有被Player
持有的旗帜
Container_GoalPoint
:定义GoalPoint
为目标点
Container_Player
:每个Player
持有一个Container
;
细想一下, WorldFlag
、GoalPoint
的具体业务逻辑和具体的某个 Instance
没有太大关系,只需要通过 ID
串联即可,所以对于 WorldFlag
、GoalPoint
,不需要真的在每一个 Instance
都创建对应 Container
,只需要一个单局管理的唯一 Manager
用来代替管理即可。但显然对于每个 Player
都需要有自己的 Container
。
可以在单局内创建全局唯一的 Container_WorldFlag
、Container_GoalPoint
实例。
flowchart TD F0[FlagSceneItem_0] F1[FlagSceneItem_1] F2[FlagSceneItem_2] subgraph WorldFlag[Container_WorldFlag] direction LR F0-.-F1-.-F2 end G0([GoalPoint_0]) G1([GoalPoint_1]) G2([GoalPoint_2]) subgraph GoalPoint[Container_GoalPoint] direction LR G0-.-G1-.-G2 end P0[[Container_Player_0]] P1[[Container_Player_1]] WorldFlag--PickUp--->P0--Drop--->WorldFlag P0--Reach--->GoalPoint P0<--Strike--->P1
这样就可以将 交互 的逻辑拆得非常简单了:
PickUp
:由FlagSceneItem
和Player
的Overlap
触发,将Container_WorldFlag
持有的对应该FlagSceneItem
的FlagItem
,传递给这个Player
的Conatiner_Player
Drop
: 由Player
的血量变化归零触发,将Container_Player
上的所有FlagItem
转移给Cotainer_WorldFlag
Reach
:由Player
和GoalPointSceneItem
的Overlap
触发,将Container_Player
上的所有FlagItem
转移给Cotainer_GoalPonit
Strike
:由Player0
和Player1
的撞击触发,将Container_Player
上的所有FlagItem
转移给Cotainer_Player1
这里的 PickUp
、Drop
、Reach
、Strike
是这张图的 边,将其定义为 TransferPolicy
。
对于 Container
,有各自的 Add(FlagItem)
、Remove(FlagItem)
,一些基本逻辑:
Container_WorldFlag
:
AddItem
:从FlagItem
上取出位置,将Item
血量设置为满血,通过调用FlagSceneItemManager
创建出FlagItem
对应的FlagSceneItem
;
RemoveItem
:调用FlagSceneItemManager
删除对应的FlagItem
对应的FlagSceneItem
;
Container_GoalPoint
:
AddItem
:判断是从哪个Container
来的,给对应Player
加分,并且RemoveItem
;
RemoveItem
:销毁对应的FlagItem
;
Container_Player
:
AddItem
:从FlagItem
获得FlagSyncData
同步给自己,并设置Player
血量为Item
的血量等逻辑;
RemoveItem
:将Player
血量设置给Item
血量,清空同步信息等逻辑;
完整的业务拆分完毕,这下需要哪些系统显而易见:
ProcessSystem
:负责管理单局流程,创建比赛与推动玩法进程;
TeamSystem
:负责Player
的组队,管理Memebr
、Score
等;
FlagItemSystem
:负责管理所有FlagItem
实例,提供Create
、Destroy
等方法,维护Item
基本的生命周期;
ContainerSystem
:负责提供基本的CreateContainer
、DestoryContainer
方法,对于具体的Container
,需要支持AddItem
、RemoveItem
、GetItem
等;
TransferSystem
:负责管理所有的TransferPolicy
,提供TransferItem
方法;
FlagSpawnerSystem
:旗帜的刷新点,负责WorldFlag
的生成,生成FlagItem
并Add
到Container_WorldFlag
;
SceneItemSystem
:维护场景里SceneItem
的生成,提供Create
、Destroy
对应的SceneItem
的方法;对于某个具体的SceneItem
,需要实现基础的Overlap
、Sync
;
具体实现
ProcesSystem
首先需要一个挂在 GameState
上的 MainProcess
负责管理全局的流程,串联多个 RaceProcess
推进玩法;
显然,一个最简单的流程可以这样:
flowchart LR M[MainProcess] R0[RaceProcess_0] R1[RaceProcess_1] F[Finish] M---->|1. 开赛|R0 R0-.->|2. Result_0|M M-->|3. Check_Result_0|R1 R1-.->|4. Result_1|M M-->|5. Check_Result_1|F R0-.->R1 R1-.->F
但是这样循环流程需要在 Main
里面关注 RaceProcess
给出的 Result
事件,显然有点不够优雅。
发现这里的主要由两部分组成:
RaceProcess
管理子比赛流程,给出Result
MainProcess
接受子比赛Result
,校验并判断开启下一场Race
或者结束比赛
所以可以将其拆成两个 Controller
:
MainProcess
负责提供 Dispatch
的功能,
FinishRace_Controller
:监听Dispatcher
的开赛
事件,并向Dispatcher
分发单局结束
事件;CreateRace_Controller
:监听单局结束
事件,进行处理并向Dispatcher
分发开赛
事件;
flowchart TB M[MainProcess] D[Dispatcher] F[FinishRace_Controller] C[CreateRace_Controller] M-->D F-->|Send_Finsih|D D-.->|Register_Create|F C-->|Send_Create|D D-.->|Register_Finish|C
TeamSystem
支持玩家的 加入
、退出
队伍,以及维护队伍的各种数据(比如 Members
、Score
),并维护数据,进行同步。
SceneItemSystem
classDiagram UCFSceneItemUtils..>UCFSceneItemManager UCFSceneItemManager..>UCFScecneItemBase class UCFSceneItemManager { frien UCFSceneItemUtils - CreateItem(Type, UID, Params) - DestroyItem(UID) } UCFScecneItemBase..*UCFScecneItemSyncComponent UCFScecneItemBase..*UCFScecneItemDisplayComponent class UCFScecneItemBase { SyncComponent : UCFScecneItemSyncComponent OverlapDelegate # OnInit() # OnUninit() # BeginDetect() # EndDetect() + CollectSyncData() } UCFScecneItem_WorldFlag--|>UCFScecneItemBase UCFScecneItem_GoalPoint--|>UCFScecneItemBase
首先,需要 UCFSceneItemManager
负责管理所有的 SceneItem
,同时通过 UCFSceneItemUtils
提供方法给外部调用。
对于一个具体的 SceneItem
,额外对其提供两个 Component
,
SyncComponent
:在DS/Client
生成,维护SceneItem
的同步数据,这一部分数据不会被AOI
裁剪,保证远距离的数据同步;DisplayComponent
:仅在Client
生成,随着SceneItem
被AOI
,用于实现客户端表现。
FlagItemSystem
、ContainerSystem
classDiagram direction LR UCaptureFlagItemSystem..>FCaptureFlagItem class UCaptureFlagItemSystem { ItemInstances : TMap~uint64|TSharedPtr~FCaptureFlagItem~~ + CreateItem(ItemID) + DestroyItem(ItemUID) + GetItem(ItemUID) - GenerateUID() - RegisterClearTimer() - UnregisterClearTimer() - ClearAllItems(bCheckUnused) } %% ----- FlagItem ----- namespace FlagItem { class FCaptureFlagItem class FCaptureFlagItemSyncData } FCaptureFlagItem..FCaptureFlagItemSyncData FCaptureFlagItem..UCaptureFlagContainerBase class FCaptureFlagItem { + InitItem(InUID, ItemID) + GetSyncData() : FCaptureFlagItemSyncData + UID / ItemID / Type / Health / MaxHealth... + Container : TWeakObjectPtr~UCaptureFlagContainerBase~ + TransferReason : ECFTransferReason } class FCaptureFlagItemSyncData { + UID / ItemID / Type / Health / MaxHealth / TransferReason } %% ----- FlagContainer ----- namespace FlagContainer { class UCaptureFlagContainerBase class UCaptureFlagContainer_WorldFlag class UCaptureFlagContainer_GoalPoint class UCaptureFlagContainer_Player } class UCaptureFlagContainerBase { Items : TMap ~uint64|TWeakPtr~FCaptureFlagItem~~ Owner : TWeakObjectPtr~UObject~ + Init(InOwner) + Uninit() + AddItem(TWeakPtr~FCaptureFlagItem~Item, Params) + RemoveItem(TWeakPtr~FCaptureFlagItem~ Item) + VerifyCanAddItem() + GetType() : ECFContainerType # OnInit() # OnUninit() # OnAddItem(TWeakPtr~FCaptureFlagItem~Item, Params) # OnRemoveItem(TWeakPtr~FCaptureFlagItem~ Item) } UCaptureFlagContainer_WorldFlag--|>UCaptureFlagContainerBase UCaptureFlagContainer_GoalPoint--|>UCaptureFlagContainerBase UCaptureFlagContainer_Player--|>UCaptureFlagContainerBase class UCaptureFlagContainer_Player{ # GetType() : ECFContainerType } %% ----- ContainerOwner ----- namespace ContainerOwner { class UCaptureFlagManager class UCaptureFlagComponent class ICaptureFlagContainerOwnerInterface } UCaptureFlagManager..>UCaptureFlagContainer_GoalPoint UCaptureFlagManager..>UCaptureFlagContainer_WorldFlag UCaptureFlagManager..|>ICaptureFlagContainerOwnerInterface class UCaptureFlagManager{ - WorldFlagContainer - GoalPointContainer + GetWorldFlagContainer() + GetGoalPointContainer() } UCaptureFlagComponent..|>ICaptureFlagContainerOwnerInterface UCaptureFlagComponent..>UCaptureFlagContainer_Player UCaptureFlagComponent..>FCaptureFlagItemSyncData class UCaptureFlagComponent{ - FlagContainer_Player - FlagItemSyncData : FCaptureFlagItemSyncData + GetCaptureFlagContainer() + UpdateFlagItemSyncData(SyncData) } ICaptureFlagContainerOwnerInterface..>UCaptureFlagContainerBase class ICaptureFlagContainerOwnerInterface{ CreateFlagContainer(InOwner) DestroyFlagContainer(Container) }
首先,需要一个 FlagItemSystem
负责管理所有的 FlagItem
实例。提供基本的创建、销毁、查询功能的同时,这个 FlagItemSystem
需要 RegisterClearTimer
,每隔一段时间,Clear
不被任何一个 Container
持有的 Item
。
1 | // UCaptureFlagItemManager |
对于 FlagItem
,保存 Flag
最基本的信息,同时提供一个 CollectSyncData
,用于生成对应的 FlagSyncData
,表示需要同步的数据,在 ContainerOwner
需要的时候进行数据同步。
1 | // FlagItem.h |
需要 Container
来持有 FlagItem
,在 AddItem
和 RemoveItem
内写具体的逻辑。
每种 Container
通过重载 UCaptureFlagContainerBase
的 PURE_VIRTUAL
方法 GetType
来定义其类型。
1 | // ContainerBase.h |
1 | // ContainerBase.cpp |
每个 Container
需要一个 ContainerOwner
来持有,这里有三种 Container
。
Container_WorldFlag
: 由CaptureFlagManager(GS)
持有,一场子比赛持有一个;Container_GoalPoint
:由CaptureFlagManager(GS)
持有,一场子比赛持有一个;Container_Player
:由CaptureFlagComponent(PS)
持有,每个Player
持有一个;
提供一个 ICaptureFlagContainerOwnerInterface
用于支持 Create
、Destroy
对应的 Container
方法;
1 | // ContainerOwner.h |
TransferSystem
classDiagram direction LR UCFTransferSystem..>UCFTransferPolicyBase UCFTransferSystem..UCaptureFlagContainerBase class UCFTransferSystem { Policies : FGameSubSystemCollection~UCFTransferPolicyBase~ friend UCFTransferPolicyBase - TransferItem(Item, TargetContainer, TransferReason) } UCFTransferPolicyBase..UCaptureFlagContainerBase class UCFTransferPolicyBase{ # OnInit() # OnUninit() # TransferItem(Item, TargetContainer) # GetTransferReason() : ECFTransferReason } UCFTansferPolicy_PickUp--|>UCFTransferPolicyBase UCFTansferPolicy_Drop--|>UCFTransferPolicyBase UCFTansferPolicy_Reach--|>UCFTransferPolicyBase UCFTansferPolicy_Strike--|>UCFTransferPolicyBase
首先需要一个 UCFTransferSystem
持有 Polices
,管理并维护所有 TransferPolicy
的生命周期,具体可以参考 [UE] GameSubSystem 简单实现。
对于一个具体的 TransferPolicy
,通过重载 TransferPolicyBase
的 PURE_VIRTUAL
方法 GetTransferReason
,明确其对应的 TransferReason
,在其 OnInit
、OnUninit
,监听该类型需要的事件,通过对应的 Callback
调用到 Base
提供的 TransferItem
方法,执行 Item
的转移。
对于一次成功的 Transfer
,显然会调用到 SourceContainer
的 Remove
,以及 TargetContainer
的 Add
。
1 | // TransferManager |
1 | // TransferPolicyBase.h |
1 | // TransferPolicy.cpp |